// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"

	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/property"
)

// Map is a bag of config stored in the settings file.
type Map map[Key]Value

// Decrypt returns the configuration as a map from module member to decrypted value.
func (m Map) Decrypt(decrypter Decrypter) (map[Key]string, error) {
	ctx := context.TODO()

	if decrypter == NopDecrypter {
		result := map[Key]string{}
		for k, v := range m {
			vv, err := v.Value(decrypter)
			if err != nil {
				return nil, err
			}
			result[k] = vv
		}
		return result, nil
	}

	objectMap := map[Key]object{}
	for k, v := range m {
		obj, err := v.unmarshalObject()
		if err != nil {
			return nil, err
		}
		objectMap[k] = obj
	}

	plaintextMap, err := decryptMap(ctx, objectMap, decrypter)
	if err != nil {
		return nil, err
	}

	result := map[Key]string{}
	for k, pt := range plaintextMap {
		v, err := pt.marshalText()
		if err != nil {
			return nil, err
		}
		result[k] = v
	}
	return result, nil
}

func (m Map) Copy(decrypter Decrypter, encrypter Encrypter) (Map, error) {
	ctx := context.TODO()

	objectMap := map[Key]object{}
	for k, v := range m {
		obj, err := v.unmarshalObject()
		if err != nil {
			return nil, err
		}
		objectMap[k] = obj
	}

	plaintextMap, err := decryptMap(ctx, objectMap, decrypter)
	if err != nil {
		return nil, err
	}

	newObjectMap, err := encryptMap(ctx, plaintextMap, encrypter)
	if err != nil {
		return nil, err
	}

	result := Map{}
	for k, obj := range newObjectMap {
		v, err := obj.marshalValue()
		if err != nil {
			return nil, err
		}
		result[k] = v
	}
	return result, nil
}

// SecureKeys returns a list of keys that have secure values.
func (m Map) SecureKeys() []Key {
	var keys []Key
	for k, v := range m {
		if v.Secure() {
			keys = append(keys, k)
		}
	}
	return keys
}

// HasSecureValue returns true if the config map contains a secure (encrypted) value.
func (m Map) HasSecureValue() bool {
	for _, v := range m {
		if v.Secure() {
			return true
		}
	}

	return false
}

// AsDecryptedPropertyMap returns the config as a property map, with secret values decrypted.
func (m Map) AsDecryptedPropertyMap(ctx context.Context, decrypter Decrypter) (property.Map, error) {
	objectMap := map[Key]object{}
	for k, v := range m {
		obj, err := v.coerceObject()
		if err != nil {
			return property.Map{}, err
		}
		objectMap[k] = obj
	}

	plaintextMap, err := decryptMap(ctx, objectMap, decrypter)
	if err != nil {
		return property.Map{}, err
	}

	result := map[string]property.Value{}
	for k, pt := range plaintextMap {
		result[k.String()] = pt.PropertyValue()
	}
	return property.NewMap(result), nil
}

// Get gets the value for a given key. If path is true, the key's name portion is treated as a path.
func (m Map) Get(k Key, path bool) (_ Value, ok bool, err error) {
	// If the key isn't a path, go ahead and lookup the value.
	if !path {
		v, ok := m[k]
		return v, ok, nil
	}

	// Otherwise, parse the path and get the new config key.
	p, configKey, err := parseKeyPath(k)
	if err != nil {
		return Value{}, false, err
	}

	// If we only have a single path segment, go ahead and lookup the value.
	root, ok := m[configKey]
	if len(p) == 1 {
		return root, ok, nil
	}

	obj, err := root.unmarshalObject()
	if err != nil {
		return Value{}, false, err
	}
	objValue, ok, err := obj.Get(p[1:])
	if !ok || err != nil {
		return Value{}, ok, err
	}
	v, err := objValue.marshalValue()
	if err != nil {
		return v, false, err
	}
	return v, true, nil
}

// Remove removes the value for a given key. If path is true, the key's name portion is treated as a path.
func (m Map) Remove(k Key, path bool) error {
	// If the key isn't a path, go ahead and delete it and return.
	if !path {
		delete(m, k)
		return nil
	}

	// Otherwise, parse the path and get the new config key.
	p, configKey, err := parseKeyPath(k)
	if err != nil {
		return err
	}

	// If we only have a single path segment, delete the key and return.
	root, ok := m[configKey]
	if len(p) == 1 {
		delete(m, configKey)
		return nil
	}
	if !ok {
		return nil
	}

	obj, err := root.unmarshalObject()
	if err != nil {
		return err
	}
	err = obj.Delete(p[1:], p[1:])
	if err != nil {
		return err
	}
	root, err = obj.marshalValue()
	if err != nil {
		return err
	}
	m[configKey] = root
	return nil
}

// Set sets the value for a given key. If path is true, the key's name portion is treated as a path.
func (m Map) Set(k Key, v Value, path bool) error {
	// If the key isn't a path, go ahead and set the value and return.
	if !path {
		m[k] = v
		return nil
	}

	// Otherwise, parse the path and get the new config key.
	p, configKey, err := parseKeyPath(k)
	if err != nil {
		return err
	}

	var newV object
	if len(p) > 1 || v.typ != TypeUnknown {
		newV, err = v.coerceObject()
		if err != nil {
			return err
		}
	}

	if len(p) == 1 {
		m[configKey] = v
		return nil
	}

	var obj object
	if root, ok := m[configKey]; ok {
		obj, err = root.unmarshalObject()
		if err != nil {
			return err
		}
	} else {
		obj = object{value: newContainer(p[1])}
	}
	err = obj.Set(p[:1], p[1:], newV)
	if err != nil {
		return err
	}
	root, err := obj.marshalValue()
	if err != nil {
		return err
	}
	m[configKey] = root
	return nil
}

func (m Map) MarshalJSON() ([]byte, error) {
	rawMap := make(map[string]Value, len(m))
	for k, v := range m {
		rawMap[k.String()] = v
	}

	return json.Marshal(rawMap)
}

func (m *Map) UnmarshalJSON(b []byte) error {
	rawMap := make(map[string]Value)
	if err := json.Unmarshal(b, &rawMap); err != nil {
		return fmt.Errorf("could not unmarshal map: %w", err)
	}

	newMap := make(Map, len(rawMap))

	for k, v := range rawMap {
		pk, err := ParseKey(k)
		if err != nil {
			return fmt.Errorf("could not unmarshal map: %w", err)
		}
		newMap[pk] = v
	}

	*m = newMap
	return nil
}

func (m Map) MarshalYAML() (any, error) {
	rawMap := make(map[string]Value, len(m))
	for k, v := range m {
		rawMap[k.String()] = v
	}

	return rawMap, nil
}

func (m *Map) UnmarshalYAML(unmarshal func(any) error) error {
	rawMap := make(map[string]Value)
	if err := unmarshal(&rawMap); err != nil {
		return fmt.Errorf("could not unmarshal map: %w", err)
	}

	newMap := make(Map, len(rawMap))

	for k, v := range rawMap {
		pk, err := ParseKey(k)
		if err != nil {
			return fmt.Errorf("could not unmarshal map: %w", err)
		}
		newMap[pk] = v
	}

	*m = newMap
	return nil
}

// parseKeyPath returns the property paths in the key and a new config key with the first
// path segment as the name.
func parseKeyPath(k Key) (resource.PropertyPath, Key, error) {
	// Parse the path, which will be in the name portion of the key.
	p, err := resource.ParsePropertyPathStrict(k.Name())
	if err != nil {
		return nil, Key{}, fmt.Errorf("invalid config key path: %w", err)
	}
	if len(p) == 0 {
		return nil, Key{}, errors.New("empty config key path")
	}

	// Create a new key that has the first path segment as the name.
	firstKey, ok := p[0].(string)
	if !ok {
		return nil, Key{}, errors.New("first path segement of config key must be a string")
	}
	if firstKey == "" {
		return nil, Key{}, errors.New("config key is empty")
	}

	configKey := MustMakeKey(k.Namespace(), firstKey)

	return p, configKey, nil
}
